Object oriented languages such as C++, Java and the like have emerged as the preferred tools for programmers. These widely used languages allow data structures and algorithms to be expressed in a way that is easy to understand. Another motivation for their use is that they can be expected to execute in a predictable way on many different computing platforms.
In another aspect, these languages have been widely adopted because of their so-called modularity. In particular, they are structured to encourage program development such that a commonly required set of operations is encapsulated in a separately named subroutine procedure, or object oriented “method.” Once coded, such methods can be reused by calling them from any point in a main program. Furthermore, methods may “call” other methods and so forth so that in most cases, an executing program will not typically be a linear sequence of instructions, but rather a hierarchical tree of called methods.
This nesting of function calls simplifies the construction of programs but at the same time complicates the execution of methods, since one or more methods may be executing at any given time. The essence of method call execution is that a called method must pass any arguments or parameters to a target function, transfer control to a memory section holding the function's executable code, “return” with the result, and at the same time, store sufficient information to ensure that subsequent execution resumes immediately after the point where the original function call was made.
This function calling mechanism, as is well known in the art, is usually implemented by pushing and popping data and memory addresses on and off a stack prior to, during, and after the call. The stack is located in a dedicated portion of memory organized as a last in, first out data memory that is not normally manipulated directly by the programmer. Rather, its contents are changed only as a result of function calls under control of the operating system. Programmers therefore do not have direct access to this section of memory but do have access to another portion of memory, called the “heap”, which is shared among threads.
Multitasking operating systems can thus adequately handle the parallel execution of most methods properly by managing both the heap and the stack. However, the need to handle a processing exception adds a layer of complexity. Such an exception occurs when a method encounters a logical situation in which it does not know how to further proceed. Even a simple method should predictably terminate if something unexpected happens, if a user does something foolish, if a resource that is expected to be available becomes unavailable or temporarily out of service, or even if a change to code inadvertently violates an assumption. One such unexpected situation might occur, for example, if while performing a division operation, a zero divisor is detected. In this instance, the arithmetic routine itself might know how to handle the operation. In other instances it might not know what to do, and enter an exception state. In a more catastrophic situation, a typical method may not know what to do, for example, if a memory fault occurs. In this instance, one would typically want the method to stop executing immediately.
Most exceptions that are attempted to be handled by programmers fall in between these two extremes. When an exception occurs, the currently executing code is in typically the best position to at least help to determine what the problem is. It can, for example, record any information that might help other parts of the program to recover, or for a human user using a debugger to figure out what has happened. So, for example, in the case of a divide by zero error, it would potentially record what the expression was whose value was zero that caused the error. In the case of a memory fault, it may be important to know what memory address was being accessed.
This information, what kind of thing went wrong, and the kind of specific additional information that might be used for figuring out what the problem was in order to correct it at some higher level, is encapsulated in a special kind of object known as an “exception object.” There are many types of standard exception objects in Java, such as Pointer Exceptions, or Illegal Argument Exceptions, and the like. Furthermore, Java programs are allowed to define exception types of their own using inheritance properties.
LJava has an exception handling mechanism known as “throw” and “catch.” A particular statement-type, “throw”, is introduced to help define how an exception is to be handled. The throw statement abruptly terminates execution of the present method, and causes the exception object to propagate backwards along a return path to the calling routine. The exception object may propagate through all hierarchy levels until a compatible “catch” statement is encountered.
The effect of a throw statement is that execution abruptly returns on the call path until a method with a compatible “catch” clause is encountered. Thus, a throw statement looks a lot like a subroutine “return” statement with an argument. The argument must be some object that is legal to throw.
The catch clause contains statements which receive the passed object as arguments, and which are executed if and only if an appropriate type of exception is thrown. The parameter to the catch clause determine the appropriate exception type. Inside the catch clause body, a parameter name is used to refer to the exception. A number of catch clauses can be associated with a single “try” statement according to the syntax rules of Java.
The throw-catch mechanism works well if the thread that throws the exception is also the same thread that catches it. This is a case of a so-called synchronous exception in the sense that the same thread that throws the exception ends up determining how to handle it.
However, certain emerging standards for Java operating environments such as the Real Time Specification for Java (RTSJ) dictate that the operating environment must be able to accommodate a situation where an exception may be thrown at a thread from another thread. In particular, in an environment where many threads may be executing in real time, each thread must accommodate the possibility that exceptions may be thrown at it from other threads. This markedly complicates management of the control flow during the handling of a thrown object.
In a reference implementation given for RTSJ, it is suggested that an interpreter should check for an Asynchronously Interrupted Execution (AIE) while executing every bytecode (instruction), e.g., determine whether any other thread is attempting to throw an exception at it. If this is the case, then the interpreter forces the thread to throw the exception at itself.
While this approach will work, it is particularly onerous. First of all, having to check for an AIE at each and every bytecode is inefficient. In fact, it is estimated that to implement this scheme might potentially double the cost of executing bytecodes. Thus program speed could be reduced by as much as one-half if this approach is taken. Such a marked reduction in program speed is of particular concern in real time systems.
Other approaches could be taken, such that exception testing is only done on a periodic basis or only for certain types of bytecodes. For example, a bytecode interpreter could be instructed to check whether an exception has been thrown on every third instruction or only on branch instructions. It turns out, however, that counting instructions to determine if one is on the third instruction is almost as slow as checking at every bytecode. Furthermore, checking only on certain types of branch instructions could violate timing constraints in the RTSJ if a section of code is encountered which has many instructions between branches.